昨天我們把 Application Server 從一台擴展成多台,靠 Load Balancer 分攤流量:

這解決了單台 Server 的容量問題,但當時刻意跳過了一個細節:
如果同一個使用者的 Request,被分配到不同的 Server,這些 Server 還認得出他是誰嗎?
回想 Day 2,我們是把 Session 直接存在 Server 的 Memory 裡:
Server
├── Application
└── Session(存在 Memory 裡)
├── Pikachu
├── Charmander
└── Squirtle
這種把使用者狀態保存在 Server 本機的做法,稱為 Stateful Server。在只有一台 Server 的時候完全沒問題,但現在情況不一樣了。
假設 Pikachu 送出第一個 Request,被 Load Balancer 分配到 Server A,Server A 在自己的 Memory 裡建立了 Pikachu 的 Session。
沒過多久,Pikachu 又送出第二個 Request,這次被分配到 Server C,但 Server C 的 Memory 裡根本沒有 Pikachu 的 Session,於是 Server C 完全不知道「這個 Request 是 Pikachu 發的」,可能就把他當成未登入使用者。
這就是 Stateful Server 在 Horizontal Scaling 之下最典型的問題。
一個很直覺的方法:既然 Pikachu 的 Session 在 Server A,那就讓 Load Balancer 永遠把 Pikachu 的 Request 導回 Server A,這就是 Sticky Session。
若沿用前一天銀行櫃檯來比喻,Sticky Session 有點像是專屬負責櫃檯的概念。
這個做法的好處是完全不用改 Application,只要在 Load Balancer 上設定「同一個使用者固定導向同一台 Server」即可。但它也帶來幾個新麻煩:
如果 Server A 突然壞掉,Load Balancer 只好把 Pikachu 轉去 Server B,但 Server B 沒有他的 Session,Pikachu 的登入狀態就這樣憑空消失了。
假設剛好很多活躍使用者都黏在 Server A 上:
Server A → 非常忙
Server B → 很閒
Server C → 很閒
即使有三台 Server,實際負載可能還是分配不均。
因為 Session 散落在各台 Server 的 Memory 裡,要新增或移除 Server 時,都得先想清楚「原本那些 Session 該怎麼辦」,讓 Scaling 這件事變得綁手綁腳。
既然問題出在「Session 跟特定 Server 綁在一起」,那反過來想:如果任何一台 Server 都能處理任何使用者的 Request 呢?
Pikachu
↓
Load Balancer
↓
┌────────┬────────┬────────┐
│ Server │ Server │ Server │
└────────┴────────┴────────┘
任何一台都能處理 Pikachu 的 Request
這就是 Stateless Server 的概念:Application Server 不再依賴自己本機保存的使用者狀態來處理 Request。
要注意的是,Stateless 不代表「系統完全沒有 State」,而是把 State 從 Server 內部,搬到 Server 外部一個大家都能存取的地方。
最直覺的做法是把 Session 存進 Database,這樣不管 Request 被分配到哪一台 Server,都能查到同一份資料。但每次都查 Database 太慢,於是更常見的做法是把 Session 放進 Distributed Cache(例如 Redis)。
Session 本身也可以定義出簡單的欄位與型別:
任何一台 Server 收到 Request 後,都拿著 session_id 去 Distributed Cache 查一次,就能知道這是誰的 Request。
這也代表 Distributed Cache 除了能存放 Application Data(後面 Day 8 會談到的 Cache 就是這種用途),也能拿來保存需要跨 Server 共享的 Runtime State。
另一種思路是乾脆不讓 Server 保存 Session,改讓 Client 自己帶著能證明身份的 Token,這就是 Token-based Authentication。
Server 驗證身份成功後,發一個經過簽章的 Token(例如 JWT)給 Client,把「使用者是誰、有什麼權限」等資訊直接封裝進 Token 本身,並用密碼學簽章確保內容不能被竄改。
之後 Client 每次 Request 都帶著這個 Token,Server 只要驗證簽章就能確認身份,完全不用查 Database 或 Cache。
整個流程畫成示意圖大概像這樣:
Client Server
│ │
│──── 1. 帳號密碼登入 ───────────────────→ │
│ │ 驗證身份成功
│ │ 簽發 JWT(簽章)
│←──── 2. 回傳 JWT ─────────────────────── │
│ │
│ Client 自行保存 JWT,之後不再需要密碼 │
│ │
│──── 3. Request + JWT ──────────────────→│
│ Authorization: Bearer <JWT> │ 只驗證簽章是否正確
│ │ 不用查 DB / Cache
│←──── 4. Response ───────────────────────│
因為身份資訊已經編碼在 Token 裡,Server 收到 Request 後不需要像方法一那樣再問一次 Distributed Cache,單靠驗證簽章就能確認「這確實是 Server 自己發出去的 Token,內容沒被竄改」。
這個做法很方便,卻有一個明顯代價:Server 失去了即時控制權。
Token 一旦發出去,在過期之前它就是有效的,如果 Pikachu 的帳號被停權,或他自己改了密碼,Server 沒辦法主動讓已經發出去的 Token 失效。
實務上通常會搭配方法一來補這個洞:
整理一下 Stateless 的好處:
在 Stateless 架構下,Server 本機還是可以保留一些東西,但僅限於跟特定使用者無關、也不影響水平擴充的資料,例如:
加上共用的 Session Store 之後,架構長這樣:

今天解決的問題是:Server 從一台變成很多台之後,怎麼讓任何一台都能處理任何使用者的 Request?
解法是採用 Stateless Architecture,把原本放在 Server Memory 裡的 Session,搬到 Server 外部的 Distributed Cache(或改用 Token-based Authentication)。這樣一來,Server 才真正具備自由增加、刪除、汰換的能力,而不會被 Session 卡住。
不過眼尖的話會發現,架構圖裡的 Database 從頭到尾都還是只有一台。Server 這一層已經能自由水平擴展了,如果所有 Server 最後都要打同一個 Database,它會不會變成下一個瓶頸?這是明天要處理的問題。